/**
 * jQuery Plasma Plug-in, version 0.4
 * Copyright (c) 2011 Daniel MacDonald dpm@projectatomic.com ALL RIGHTS RESERVED
 * 
 * This is free software; you can redistribute it and/or modify 
 * it under the terms of the GNU Lesser General Public License 
 * as published by the Free Software Foundation; either version 
 * 2.1 of the License, or (at your option) any later version.
 * 
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * 
 * This plug-in creates a table and cycles the background colors
 * of the table cell with an HSV Sine transformation. This plug-in
 * can become processor intensive with too many rows or columns,
 * which could cause a browser to react slowly, freeze, or crash.
 * The shift and blur options seem to have the most noticeable
 * effect on the plasma.
 * 
 * VERSION 0.4 (Beta 2)
 * added overlay for image or scrolling text
 * removed limit to time shift
 * TODO: overlay position
 * 
 * VERSION 0.3 (Beta 1)
 * added different plasma algorithms settings.type
 * added a limit to the time shift parameter
 * 
 * VERSION 0.2
 * optimized the delay timer
 * added start, stop, destroy methods
 * added data support for settings to allow on the fly changes
 * 
 * VERSION 0.1
 * initial release, not ready for prime time
 * 
 * ROADMAP:
 * fix bugs
 * work on performance
 */

(function( $ ){

  var settings = {
    rows: 8, // more rows = more processing cycles
    cols: 8, // more cols = more processing cycles
    spacing: 0, // table's cellspacing attribute
    padding: 0, // table's cellpadding attribute
    border: 0, // table's border attribute
    height: '40px', // css height property for each cell
    width: '40px', // css height property for each cell
    html: '', // each cell can contain some html
    overlay: false, // boolean, on or off
    overcolor: [255, 255, 255], // array of rgb values 0-255
    //overbias: [0,0],  // x,y cells to move overlay - not yet implemented
		pattern: [], // array for overlay pattern
    type: 0, // so far, 0 or 1 or 2, more to come
    shift: 0, // any positive integer
    blur: 1, // less than 1 sharpens the plasma, greater than 1 blurs
    r: 1, // red intensity 0-1
    g: 1, // green intensity 0-1
    b: 1, // blue intensity 0-1
    h: 255, // hue multiplier
    s: 1, // saturation intensity 0-1
    v: 1, // value intensity 0-1
    delay: 100, // milliseconds between frame refresh
    speed: 100 // scroll speed milliseconds
  };
 
  var $this;
 
  var methods = {

/*
 * Initialize plasma, creates table and sets timeout for plasma_morph
 * 
 * @this [jQuery object}
 * @param {object} options
 * @return {void}
 */
    init: function(options) {
     
      return this.each(function(){
        $this = $(this);
        if (options) { 
          $.extend(settings, options);
        };
        var table = $('<table/>').attr({cellpadding:settings.padding, cellspacing:settings.spacing, border:settings.border});
        for (i = 0; i < settings.rows; i++) {
          var row = $('<tr/>');
          for (j = 0; j < settings.cols; j++) {
            row.append($('<td/>').css({height:settings.height, width:settings.width}).html(settings.html));
          };
          table.append(row);
        };
        $this.append(table);
        $this.data('plasma', settings);
        methods.plasma_morph();
      });
    },

/*
 * Performs transformations on each table cell, runs every settings.delay milliseconds
 * 
 * @return {void}
 */   
    plasma_morph: function() {
      var data = $this.data('plasma');
      for(y = 0; y < data.rows; y++) {
        for(x = 0; x < data.cols; x++) {
          var color = methods.set_color(x, y);
          methods.set_cell(x, y, color);
        };
      };
      data.shift++;
      $.extend($this.data('plasma'), {timeout: window.setTimeout(methods.plasma_morph, data.delay), shift: data.shift});
    },
  
/*
 * Performs Sine wave transformation, cycles HSV colors
 * 
 * @param {integer} x The table column index
 * @param {integer} y The table row index
 * @return {object} color Object with r, g, b properties {float} 0-255
 */    
    set_color: function(x, y) {
      var data = $this.data('plasma');
      var distance = function(x1, y1, x2, y2){
        return Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
      };
      switch (data.type) {
        case 1: // blur sines and hsv conversion
          var value = Math.sin(distance(x + data.shift, y, 128, 128) / data.cols / data.blur) 
            + Math.sin(distance(x, y, 64, 64) / data.cols / data.blur) 
            + Math.sin(distance(x, y, 192, 64) / (data.rows - 1) / data.blur) 
            + Math.sin(distance(x, y + data.shift/ (data.rows - 1), 192, 100) / data.rows / data.blur);
          var color = methods.HSVtoRGB(value * data.h / data.blur, data.s, data.v);
          break;
        case 2: // blur just hsv conversion
          var value = Math.sin(distance(x + data.shift, y, 128, 128) / 8) 
            + Math.sin(distance(x, y, 64, 64) / 8) 
            + Math.sin(distance(x, y, 192, 64) / 7) 
            + Math.sin(distance(x, y + data.shift / 7, 192, 100) / 8);
					var color = methods.HSVtoRGB(value * data.h / data.blur, data.s, data.v);
          break;
        default: // no adjustments
          var value = Math.sin(distance(x + data.shift, y, 128, 128) / 8) 
            + Math.sin(distance(x, y, 64, 64) / 8) 
            + Math.sin(distance(x, y, 192, 64) / 7) 
            + Math.sin(distance(x, y + data.shift / 7, 192, 100) / 8);
          var color = methods.HSVtoRGB(value * data.h, data.s, data.v);
      };
      color.r *= data.r;
      color.g *= data.g;
      color.b *= data.b;
      return color;
    },

/*
 * Sets the background color of table cell after applying overlay transformation
 * 
 * @param {integer} x The table column index
 * @param {integer} y The table row index
 * @param {object} color Object with r, g, b properties {float} 0-255
 * @return {void}
 */    
    set_cell: function(x, y, color) {
			var data = $this.data('plasma');
      if (data.overlay === true && typeof data.pattern[y] !== 'undefined' && typeof data.pattern[y][x] !== 'undefined') {
				if ($.isArray(data.pattern[y][x]) && data.pattern[y][x].length == 3) {
					color.r = data.pattern[y][x][0];
					color.g = data.pattern[y][x][1];
					color.b = data.pattern[y][x][2];
				} else if (data.pattern[y][x] == 1) {
          color = methods.overlay_color(color);
				}; 
      }; 
      if (data.overlay === true && typeof data.pattern == 'string') {
        color = methods.overlay_char(x, y, color);
			};
			
      var r = color.r > 255 ? 255 : color.r < 0 ? 0 : Math.floor(color.r);
      var g = color.r > 255 ? 255 : color.g < 0 ? 0 : Math.floor(color.g);
      var b = color.b > 255 ? 255 : color.b < 0 ? 0 : Math.floor(color.b);
      
      $this.find('tr').eq(y).find('td').eq(x).css('background-color', 'rgb('+r+','+g+','+b+')');
    },

/*
 * Returns RGB equivalent of HSV value
 * 
 * @param {integer} h Hue 0-359
 * @param {float} s Saturation 0-1
 * @param {float} v Value 0-1
 * @return {object} color Object with r, g, b properties {float} 0-255
 */    
    HSVtoRGB: function(h,s,v) { 
      var data = $this.data('plasma');
      var r,g,b,i,f,p,q,t;
      while (h < 0) { 
        h += 359; 
      };
      h %= 359;
      
      s = s > 1 ? 1 : s < 0 ? 0 : s;
      v = v > 1 ? 1 : v < 0 ? 0 : v;
      
      if (s == 0) {
        r = g = b = v;
      } else {
        h /= 60;
        f = h - (i = Math.floor(h));
        p = v * (1 - s);
        q = v * (1 - s * f);
        t = v * (1 - s * (1 - f));
        switch (i) {
          case 0: r = v; g = t; b = p; break;
          case 1: r = q; g = v; b = p; break;
          case 2: r = p; g = v; b = t; break;
          case 3: r = p; g = q; b = v; break;
          case 4: r = t; g = p; b = v; break;
          case 5: r = v; g = p; b = q; break;
        };
      };
      return {
        r: r * 255,
        g: g * 255,
        b: b * 255
      };
    },
 
/*
 * Applies overlay transformation to single cell
 * 
 * @param {object} color Object with r, g, b properties {float} 0-255
 * @return {object} color Object with r, g, b properties {float} 0-255
 */    
    overlay_color : function(color) {
      var data = $this.data('plasma');
      if ($.isArray(data.overcolor)) {
        color = {
          r: data.overcolor[0],
          g: data.overcolor[1],
          b: data.overcolor[2]
        };
      } else if (typeof data.overcolor == 'number') {
        color = {
          r: color.r * data.overcolor,
          g: color.g * data.overcolor,
          b: color.b * data.overcolor
        };
      };
      return color;
    },

/*
 * Sets up character scroller if not started, or applies overlay transformation to current frame
 * overlay_char method (semi-private)
 * x, y x/y coordinates, color object with r, g, b properties 0-255 
 * returns object with r, g, b properties 0-255
 */    
    overlay_char : function(x, y, color) {
      var data = $this.data('plasma');
      
      if (data.overlay === true && (typeof data.scroll_timeout == 'undefined' || data.scroll_timeout == -1)) {
        methods.overlay_scroll();
      } else if (typeof data.scroll_frame != 'undefined' && typeof data.scroll_frame[y] != 'undefined') {
        if (data.scroll_frame[y][x] == 1) {
          color = methods.overlay_color(color);
        }; 
      };
      return color;
    },

/*
 * overlay_scroll method (semi-private)
 * sets data.scroll_frame (8x8 array of 1s and 0s)
 * sets data.scroll_timeout
 * 
 * requires chars.js
 */    
    overlay_scroll : function() {
      var data = $this.data('plasma');
			data.scroll_frame = new Array();
		
			if (typeof data.scroll_buffer == 'undefined' || data.scroll_buffer.length == 0) {
				data.scroll_buffer = new Array(); 
				data.scroll_offset = 0;
				
				for (i=0; i<data.rows; i++) { // i = row
					// add spaces to beginning so pattern scrolls into frame
					var row = new Array();
					for (j=0; j<data.cols; j++) {
						row.push(0);
					};
					for (j=0, m=data.pattern.length; j<m; j++) { // j = character
						var this_index = $.inArray(data.pattern[j], plasma_char_map) == -1 ? 0 : $.inArray(data.pattern[j], plasma_char_map);
						var this_char = plasma_char_data[this_index];
						if (i < this_char.length) {
							for (k=0, n=this_char[i].length; k<n; k++) { // k = column
								row.push(this_char[i][k]);
							};
						};
					};
					// add spaces to the end so message scrolls off frame
					for (j=0; j<data.cols; j++) {
						row.push(0);
					};
					data.scroll_buffer.push(row);
				};
			};	
			
			// so, buffer is built, just need to pull out the current frame
			for (i=0, l=data.scroll_buffer.length; i<l; i++) { // loop throught rows
				var row = data.scroll_buffer[i].slice(data.scroll_offset, (data.scroll_offset + data.cols));
				//console.log('row: '+row.length);
				data.scroll_frame.push(row);
			};
			
			if (data.scroll_offset < data.scroll_buffer[0].length - data.cols) {
				// still more to show
				data.scroll_offset++;
			} else {
				// we have scrolled to the end
        window.clearTimeout(data.scroll_timeout);
        data.scroll_timeout = -1;
        data.scroll_offset = -1;
        data.scroll_frame = [];
        data.overlay = false;
			};
      if (data.overlay === true) {
        data.scroll_timeout = window.setTimeout(methods.overlay_scroll, data.speed);
      };
      $.extend($this.data('plasma'), data);
    },

/*
 * start method
 * restarts a stopped animation
 */    
    start : function() {
      return this.each(function(){
        if ($(this).data('plasma')) {
          methods.plasma_morph();
        };
      });
    },

/*
 * stop method
 * clears the timeout to plasma_morph method
 */    
    stop : function() {
      return this.each(function(){
        if ($(this).data('plasma')) {
          window.clearTimeout($(this).data('plasma').timeout);
        };
      });
    },

/*
 * showover method
 * apply an overlay to the plasma
 */    
    showover : function(pattern) {
			return this.each(function(){
				$.extend($(this).data('plasma'), {overlay: true, pattern: pattern});
      });
    },

/*
 * hideover method
 * clears the overlay
 */    
    hideover : function() {
			return this.each(function(){
        var data = $this.data('plasma');
        if (typeof data.scroll_timeout != 'undefined') {
          window.clearTimeout(data.scroll_timeout);
          data.scroll_timeout = -1;
          data.scroll_offset = -1;
          data.scroll_frame = [];
        };
        data.overlay = false;
				data.pattern = [];
        $.extend($(this).data('plasma'), data);
      });
    },

/*
 * destroy method
 * clears the timeout to plasma_morph method
 * removes stored data
 */    
    destroy : function() {
      return this.each(function(){
        if ($(this).data('plasma')) {
          var data = $this.data('plasma');
          if (typeof data.scroll_timeout != 'undefined') {
            window.clearTimeout(data.scroll_timeout);
            data.scroll_timeout = -1;
            data.scroll_char = -1;
            data.scroll_offset = -1;
            data.scroll_frame = [];
          };
          data.overlay = false;
          data.pattern = [];
          window.clearTimeout(data.timeout);
          $(this).removeData('plasma').empty();
        };
      });
    }
  };

/*
 * jQuery plug-in declaration
 */
  $.fn.plasma = function(method) {
    if (methods[method]) {
      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
    } else if (typeof method === 'object' || !method) {
      return methods.init.apply(this, arguments);
    } else {
      $.error('Method ' + method + ' does not exist on jQuery.plasma');
    };   
  };

})(jQuery);
